{
"cells": [
{
"cell_type": "markdown",
"id": "0",
"metadata": {},
"source": "# Orbital Mean-Element Messages\n\nOrbital Mean-Element Messages (OMMs) are a standardized data product defined by [CCSDS](https://ccsds.org/Pubs/502x0b3e1.pdf) for exchanging satellite orbital elements in a machine-readable way. They contain the same orbital parameters as TLEs but in a more structured format, and are growing in popularity as the modern replacement for TLE distribution.\n\nOMMs are available from [CelesTrak](https://celestrak.org) and [Space-Track](https://www.space-track.org) in multiple encodings:\n\n- **JSON** — the most common and straightforward format\n- **XML** — more verbose with deeper hierarchy, but widely supported\n- **KVN** — key-value notation; imposes very little structure\n\n`satkit` supports SGP4 propagation of OMMs represented as Python dictionaries. KVN is **not** supported. The helper `sk.omm_from_url(url)` fetches an OMM endpoint, auto-detects JSON vs XML, and returns a list of dictionaries that can be passed directly to `sk.sgp4` — no external HTTP or XML libraries needed."
},
{
"cell_type": "markdown",
"id": "1",
"metadata": {},
"source": [
"## Example 1: JSON Format\n",
"\n",
"Load a JSON OMM for the International Space Station from CelesTrak, propagate with SGP4, and convert to geodetic coordinates. The JSON format maps directly to a Python dictionary, making it simple to work with."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2",
"metadata": {},
"outputs": [],
"source": "import satkit as sk\n\n# Fetch the current ephemeris for the International Space Station (ISS).\n# sk.omm_from_url auto-detects JSON vs XML response format and returns a list\n# of OMM dictionaries that can be passed directly to sk.sgp4.\nurl = \"https://celestrak.org/NORAD/elements/gp.php?CATNR=25544&FORMAT=json\"\nomm = sk.omm_from_url(url)\n\n# Get a representative time from the OMM epoch\nepoch = sk.time(omm[0][\"EPOCH\"])\n# Create a list of times — once every 10 minutes\ntime_array = [epoch + sk.duration(minutes=i * 10) for i in range(6)]\n\n# TEME (inertial) output from SGP4\npTEME, _vTEME = sk.sgp4(omm[0], time_array)\n\n# Rotate to Earth-fixed\npITRF = [sk.frametransform.qteme2itrf(t) * p for t, p in zip(time_array, pTEME)]\n\n# Geodetic coordinates of space station at given times\ncoord = [sk.itrfcoord(x) for x in pITRF]"
},
{
"cell_type": "markdown",
"id": "3",
"metadata": {},
"source": "## Example 2: XML Format\n\n`sk.omm_from_url` automatically detects the response format. Passing an XML endpoint returns the same list-of-dicts shape as the JSON version, so the downstream SGP4 call is identical — no `xmltodict` plumbing or manual tree traversal required. Here we fetch the same ISS OMM in XML, propagate it over roughly one orbit, and plot the ground track."
},
{
"cell_type": "code",
"execution_count": null,
"id": "4",
"metadata": {},
"outputs": [],
"source": "import satkit as sk\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport scienceplots # noqa: F401\nplt.style.use([\"science\", \"no-latex\", \"../satkit.mplstyle\"])\n%config InlineBackend.figure_formats = ['svg']\nimport warnings\nwarnings.filterwarnings(\"ignore\", \"Downloading\")\nimport cartopy.crs as ccrs\nimport cartopy.feature as cfeature\n\n# Fetch the current ISS ephemeris in XML format. sk.omm_from_url auto-detects\n# the format and normalizes the result to the same list-of-dicts shape as the\n# JSON example above — no xmltodict or hand-written tree walking needed.\nurl = \"https://celestrak.org/NORAD/elements/gp.php?CATNR=25544&FORMAT=xml\"\nomm = sk.omm_from_url(url)[0]\n\n# Get a representative time from the OMM epoch\nepoch = sk.time(omm[\"EPOCH\"])\n# Time array spanning roughly one orbit at 1-minute cadence\ntime_array = [epoch + sk.duration(minutes=i) for i in range(97)]\n\n# TEME (inertial) output from SGP4\npTEME, _vTEME = sk.sgp4(omm, time_array)\n\n# Rotate to Earth-fixed\npITRF = [sk.frametransform.qteme2itrf(t) * p for t, p in zip(time_array, pTEME)]\n\ncoord = [sk.itrfcoord(x) for x in pITRF]\n\nlat, lon, alt = zip(*[(c.latitude_deg, c.longitude_deg, c.altitude) for c in coord])\n\n# Break ground track at longitude discontinuities (date line crossings)\nlon_arr, lat_arr = np.array(lon), np.array(lat)\nbreaks = np.where(np.abs(np.diff(lon_arr)) > 180)[0] + 1\nlon_segs = np.split(lon_arr, breaks)\nlat_segs = np.split(lat_arr, breaks)\n\nfig, ax = plt.subplots(figsize=(10, 5), subplot_kw={\"projection\": ccrs.PlateCarree()})\nax.add_feature(cfeature.LAND, facecolor=\"lightgray\")\nax.add_feature(cfeature.BORDERS, linewidth=0.5)\nax.add_feature(cfeature.COASTLINE, linewidth=0.5)\nfor lo, la in zip(lon_segs, lat_segs):\n ax.plot(lo, la, linewidth=1.5, color=\"C0\", transform=ccrs.PlateCarree())\nax.set_title(\"ISS Ground Track\")\nax.set_global()\nplt.tight_layout()\nplt.show()\n\nfig, ax = plt.subplots(figsize=(10, 4))\nax.plot([t.datetime() for t in time_array], np.array(alt) / 1e3)\nax.set_xlabel(\"Time\")\nax.set_ylabel(\"Altitude (km)\")\nax.set_title(\"ISS Altitude vs Time\")\nfig.autofmt_xdate()\nplt.tight_layout()\nplt.show()"
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.14.0"
}
},
"nbformat": 4,
"nbformat_minor": 5
}